[Salesforce] Listのintersection(積集合)の高速化
小ネタです。
Listのintersection
Apexで二つのListのintersection(積集合、共通集合とも)はSetを使って次のように実装できます。
List<SomeObj> listA = getSomeObjListA(); // getSomeObjListA はN個のSomeObjを返すものとする List<SomeObj> listB = getSomeObjListB(); // getSomeObjListB はN個のSomeObjを返すものとする List<SomeObj> listAB = new List<SomeObj>(); // listA と listB のintersection を入れるリスト Set<SomeObj> setOfListA = new Set<SomeObj>(listA); // Setを使うことでループの計算量をO(N)にする for ( SomeObj obj : listB ) { if ( setOfListA.contains(obj) ) { listAB.add(obj); } }
計算量がO(1)のSet.containsを使うことで、forループの計算量が全体で O(N x 1) = O(N)
になっています。
これを、次のように計算量がO(N)のList.indexOfを使うと、forループの計算量が O(N x N) = O(N^2)
になってしまいます。
List<SomeObj> listA = getSomeObjListA(); // getSomeObjListA はN個のSomeObjを返すものとする List<SomeObj> listB = getSomeObjListB(); // getSomeObjListB はN個のSomeObjを返すものとする List<SomeObj> listAB = new List<SomeObj>(); // listA と listB のintersection を入れるリスト for ( SomeObj obj : listB ) { Integer index = listA.indexOf(obj); // List.indexOfは計算量がO(N)かかる if ( index != -1 ) { listAB.add(obj); } }
こうなると、N = 数百ぐらいのオーダーでSalesforceのガバナ制限の一つである、「Salesforce サーバの最大 CPU 時間」の10,000msに抵触してしまい、実行時エラーになります。
indexOfが遅いのは常にListの先頭から順に一致するかを見ていくからで、ペンキ屋のシュレミールがいるからです1。
ちなみに、Javaでは HashSet.IntersectWith
を使うという手もあります。
同様のことはApexでは Set.retainAll
で表現できますが、ここにも実装のどこかにペンキ屋のシュレミールがいるようで、まともに動きませんでした。
参考資料
- 計算量に注目してプログラムを高速化する - Qiita
- List クラス | Apex 開発者ガイド | Salesforce Developers
- Set クラス | Apex 開発者ガイド | Salesforce Developers
- Joel on Software にて、記述されているジョーク。シュレミールがペンキを塗った壁の量が日を追うごとに激減していくが、それはシュレミールがペンキの缶を初日のスタートの位置から動かさなかったからというオチ。「毎日ペンキの缶から遠くなってくんで!」 ↩